/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.core.services;

import java.io.StringReader;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.PropertyDefinition;
import javax.xml.parsers.SAXParserFactory;
import offset.nodes.Constants;
import offset.nodes.client.model.StringArrayConverter;
import offset.nodes.client.virtual.model.jcr.PropertyValueFormatter;
import offset.nodes.server.model.RepositoryModel;
import offset.nodes.server.servlet.DummyContentHandler;
import offset.nodes.server.servlet.NodeObservable;
import offset.nodes.server.servlet.NodeObserver;
import offset.nodes.server.servlet.XMLFilter;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

public class XMLToJCRUpdater extends DummyContentHandler implements ContentHandler, NodeObservable {

    Session session;
    Stack path = new Stack();
    StringBuffer content = null;
    LinkedList<NodeObserver> observers = new LinkedList<NodeObserver>();
    LinkedList<XMLFilter> filters = new LinkedList<XMLFilter>();
    // properties set in Mode.update also
    HashSet<String> settableProperties = new HashSet<String>();
    // multi value properties, that shall be extended only
    HashSet<String> extendOnlyProperties = new HashSet<String>();
    RepositoryModel model;

    public enum Mode {
        set, update
    }
    Mode mode = Mode.set;

    public XMLToJCRUpdater(Session session, Node parent) {
        super();
        this.session = session;
        this.model = new RepositoryModel(session);
        path.push(new NodeInfo(parent));
    }

    public void setMode(Mode mode) {
        this.mode = mode;
    }

    public void setParent(Node parent) {
        path = new Stack();
        path.push(new NodeInfo(parent));
    }
    
    protected String createKey(String element, String attributeName) {
        return element + Constants.PATH_SEPARATOR + attributeName;
    }

    /**
     * Usually, only in mode == Mode.set properties, that have not been
     * updated will be removed.
     *
     * However, it is possible to do so even in Mode.update for specific
     * properties. Such properties can be specified by this method.
     * 
     * @param element
     * @param attributeName
     */
    public void addSettableProperty(String element, String attributeName) {
        settableProperties.add(createKey(element, attributeName));
    }

    /**
     * Certain multi value properties shall be set, but not remove the other
     * existing values. That is, additional values shall be added, but no
     * existing values removed.
     *
     * @param addOnlyProperties
     */
    public void addExtendOnlyProperty(String element, String attributeName) {
        extendOnlyProperties.add(createKey(element, attributeName));
    }

    public void update(String template) throws RepositoryException {
        try {
            XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
            if (filters.size() > 0)
                reader.setContentHandler(filters.get(0));
            else
                reader.setContentHandler(this);
            reader.parse(new InputSource(new StringReader(template)));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RepositoryException(e);
        }
    }

    public List<Node> getUpdatedNodes() {
        if (!(path.peek() instanceof NodeInfo))
            return new LinkedList();

        NodeInfo info = (NodeInfo) path.peek();
        return info.getChildren();
    }

    public void addFilter(XMLFilter filter) {
        if (filters.size() > 0)
            filters.getLast().setOutputHandler(filter);
        filter.setOutputHandler(this);
        
        filters.add(filter);
    }

    public void addNodeObserver(NodeObserver observer) {
        observers.add(observer);
    }

    String getName(String uri, String localName, String qName) {
        return qName;
    }

    String getAttribute(Attributes atts, String name) {
        return atts.getValue(name);
    }

    String getFirstMixin(Attributes atts) {
        for (int i = 0; i < atts.getLength(); i++) {
            String name = atts.getQName(i);
            if (name.startsWith("mix:"))
                return name;
        }

        return null;
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
        if (content != null)
            content.append(ch, start, length);
    }

    class NodeInfo {

        Node node;
        List<Node> children = new LinkedList<Node>();

        public NodeInfo(Node node) {
            super();
            this.node = node;
        }

        public void addChild(Node node) {
            children.add(node);
        }

        public boolean hasChild(Node node) throws RepositoryException {
            Iterator<Node> i = children.iterator();
            while (i.hasNext()) {
                Node child = i.next();
                if (node.isSame(child))
                    return true;
            }

            return false;
        }

        public Node getNode() {
            return node;
        }

        public List<Node> getChildren() {
            return children;
        }
    }

    class File {

        String type;
        String mixin;
        Attributes atts;

        public File(String type, String mixin, Attributes atts) {
            super();
            this.type = type;
            this.mixin = mixin;
            this.atts = atts;
        }

        public String getMixin() {
            return mixin;
        }

        public String getType() {
            return type;
        }

        public Attributes getAtts() {
            return atts;
        }
    }

    public boolean isMultiple(Node node, String propertyName) throws RepositoryException {
        PropertyDefinition[] propertyDefinitions = node.getPrimaryNodeType().getPropertyDefinitions();
        for (int i = 0; i < propertyDefinitions.length; i++) {
            if (propertyDefinitions[i].getName().equals(propertyName))
                if (propertyDefinitions[i].isMultiple())
                    return true;
                else
                    return false;
        }

        return false;
    }
    
    /**
     * Empty values are not allowed for references or enumerated string values.
     * 
     * @param propertyDefinition the property definition
     * @return are empty values allowed
     */
    public boolean emptyValuesAllowed(PropertyDefinition propertyDefinition) {
        if (propertyDefinition.getRequiredType() == PropertyType.REFERENCE)
            return false;
        
        return propertyDefinition.getRequiredType() != PropertyType.STRING
                || propertyDefinition.getValueConstraints() == null 
                || propertyDefinition.getValueConstraints().length == 1; /* regular expression */
    }
    
    protected PropertyDefinition getPropertyDefinition(Node node, String propertyName) throws RepositoryException {
        PropertyDefinition[] propertyDefinitions = node.getPrimaryNodeType().getPropertyDefinitions();
        for (int i=0; i<propertyDefinitions.length; i++)
            if (propertyDefinitions[i].getName().equals(propertyName))
                return propertyDefinitions[i];
        return null;
    }
    
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        String type = getAttribute(atts, Constants.JCR_PRIMARY_TYPE);
        if (type != null && type.equals("nt:file")) {
            String mixin = getFirstMixin(atts);
            path.push(new File(type, mixin, atts));
            content = new StringBuffer();
            return;
        }
        try {
            Node node = null;
            String uuid = null;
            if ((uuid = getAttribute(atts, Constants.JCR_UUID)) != null && uuid.length() > 0 && !uuid.startsWith("{"))
                node = session.getNodeByUUID(uuid);
            else if (type != null)
                node = ((NodeInfo) path.peek()).getNode().addNode(getName(uri, localName, qName), type);
            else
                node = ((NodeInfo) path.peek()).getNode().addNode(getName(uri, localName, qName));

            if (!node.getName().equals(qName))
                session.move(node.getPath(), node.getParent().getPath() + Constants.PATH_SEPARATOR + qName);
            
            ((NodeInfo) path.peek()).addChild(node);
            path.push(new NodeInfo(node));
            for (int i = 0; i < atts.getLength(); i++) {
                String name = atts.getQName(i);
                
                if (name.startsWith("jcr:") && !name.equals(Constants.JCR_MIXIN_TYPES))
                    continue;
                
                Value value = null;
                Value[] values = null;
                
                PropertyDefinition propertyDefinition = getPropertyDefinition(node, name);
                if (propertyDefinition == null)
                    continue;
                
                if (propertyDefinition.isMultiple()) {
                    StringArrayConverter sa = new StringArrayConverter(atts.getValue(i));
                    values = PropertyValueFormatter.parse(propertyDefinition.getRequiredType(), session.getValueFactory(), sa.toArray());
                } else if ((atts.getValue(i) == null || atts.getValue(i).length() == 0)
                        && !emptyValuesAllowed(propertyDefinition))
                    value = null;
                else
                    value = PropertyValueFormatter.parse(propertyDefinition.getRequiredType(), session.getValueFactory(), atts.getValue(i));
                    
                if (name.equals(Constants.JCR_MIXIN_TYPES)) {
                    for (int j = 0; j < values.length; j++) {
                        if (!node.isNodeType(values[j].getString()))
                            node.addMixin(values[j].getString());
                    }
                } else { 
                    if (propertyDefinition.isMultiple()) {
                        if (node.hasProperty(name) && extendOnlyProperties.contains(createKey(node.getPrimaryNodeType().getName(), name))) {
                            ValueExtender extender = new ValueExtender(session.getValueFactory());
                            node.setProperty(name, extender.addValues(node.getProperty(name).getValues(), values));
                        } else
                            node.setProperty(name, values);
                    }
                    else
                        node.setProperty(name, value);
                }
            }
        } catch (Exception e) {
            throw new SAXException(e);
        }
    }

    public void endElement(String uri, String localName, String qName) throws SAXException {
        try {
            if (path.peek() instanceof File) {
                File file = (File) path.pop();
                NodeInfo info = (NodeInfo) path.peek();
                Node node = info.getNode();
                Node fileNode = model.storeBlob(node, getName(uri, localName, qName), "", content.toString().getBytes(), file.getMixin());
                info.addChild(fileNode);
                for (int i = 0; i < file.getAtts().getLength(); i++) {
                    String name = file.getAtts().getQName(i);
                    String value = file.getAtts().getValue(i);

                    if (name.equals(Constants.JCR_MIXIN_TYPES) && !fileNode.isNodeType(value))
                        fileNode.addMixin(value);
                    else if (!name.startsWith("jcr:"))
                        fileNode.setProperty(name, value);
                }
                return;
            }
            NodeInfo nodeInfo = (NodeInfo) path.pop();
            Node node = nodeInfo.getNode();
            if (mode == Mode.set || settableProperties.size() > 0) {
                PropertyIterator p = node.getProperties();
                while (p.hasNext()) {
                    Property property = p.nextProperty();
                    if (!property.isNew() && !property.isModified() && !property.getDefinition().isProtected() && !property.getDefinition().isMandatory() &&
                            (mode == Mode.set || settableProperties.contains(createKey(node.getPrimaryNodeType().getName(), property.getName()))))
                        property.remove();
                }

                if (mode == Mode.set) {
                    NodeIterator n = node.getNodes();
                    while (n.hasNext()) {
                        Node child = n.nextNode();
                        if (!nodeInfo.hasChild(child))
                            child.remove();
                    }
                }
            }

            Iterator<NodeObserver> i = observers.iterator();
            while (i.hasNext()) {
                i.next().nodeCreated(node);
            }

        } catch (RepositoryException e) {
            throw new SAXException(e);
        }
    }
}
